我在学习nexjts15的时候,好像没有听说过Cache Components,只见到过"use cache"这个命令,只知道有缓存功能。
Next.js 16(2025 年 10 月 21 日正式发布,目前最新小版本为 16.1.x)引入了 Cache Components,这是 App Router 缓存模型的一次重大变革。核心目标是让缓存行为完全显式、可预测,彻底告别之前版本中“隐式缓存规则太多、容易踩坑”的痛点。
在 next.config.ts / next.config.js 中设置:
xxxxxxxxxx71import type { NextConfig } from 'next'23const nextConfig: NextConfig = {4 cacheComponents: true, // 开启 Cache Components(包含 PPR)5}67export default nextConfig开启后,整个缓存语义会发生根本性翻转:
| 特性 | Next.js 14 ~ 15(默认行为) | Next.js 16 + cacheComponents: true |
|---|---|---|
| 默认渲染模式 | 尽可能静态(fetch 很容易被缓存) | 全部动态(request time 执行,除非显式缓存) |
| fetch() 是否自动缓存 | 是(Data Cache) | 否(必须用 use cache 包裹才缓存) |
| cookies()/headers() 影响范围 | 容易污染整条 Route 变成 dynamic | 只影响当前组件/函数,不会向上污染 |
| 想缓存某部分怎么做? | 很麻烦(force-static、unstable_cache 等 hack) | 直接加 'use cache' 指令,超级直观 |
| 静态 + 动态混合 | 依赖实验性 PPR,边界模糊 | 原生支持优秀(Partial Prerendering + Streaming) |
| 心智负担 | 高(规则隐晦、容易出意外) | 大幅降低(你要缓存就写出来,不写就是动态) |
'use cache' 是最关键的指令,可以加在三种地方:
xxxxxxxxxx61'use cache'23export default async function ProductsPage() {4 const products = await db.products.findMany() // 会被缓存5 return <ProductList products={products} />6}xxxxxxxxxx51async function Sidebar() {2 'use cache'3 const categories = await getCategories() // 只缓存这个组件4 return <SidebarUI categories={categories} />5}xxxxxxxxxx41'use cache'2export async function getUser(id: string) {3 return db.user.findUnique({ where: { id } })4}xxxxxxxxxx71import { cacheLife } from 'next/cache'23'use cache'4cacheLife('days', 7) // 内置预设:days / hours / minutes / forever 等5// 或自定义 profile(推荐在 next.config.ts 统一定义)67export async function getSettings() { }常见内置 profile:
xxxxxxxxxx81import { cacheTag } from 'next/cache'23'use cache'4cacheTag('products')56// 某处 Server Action 或 API 中失效7import { revalidateTag } from 'next/cache'8revalidateTag('products') // 软失效(stale-while-revalidate)1、默认保持动态
→ 先把 cacheComponents: true 打开,然后尽量别加 'use cache',让页面默认走 request time + streaming,这样最安全、最符合直觉。
2、只对真正不变的部分加缓存 适合缓存的典型场景:
3、动态内容坚决不碰
<Suspense> + streaming 才是正确打开方式。4、经典组合:静态壳 + 动态流式内容
xxxxxxxxxx171// layout.tsx2import StaticHeader from '@/components/StaticHeader' // 'use cache' 在里面3import DynamicUserInfo from '@/components/DynamicUserInfo'45export default function RootLayout({ children }) {6 return (7 <html>8 <body>9 <StaticHeader /> {/* 瞬间静态 */}10 <Suspense fallback={<UserSkeleton />}>11 <DynamicUserInfo /> {/* 流式加载 */}12 </Suspense>13 {children}14 </body>15 </html>16 )17}Next.js 16 的 Cache Components 把缓存决策权彻底还给了开发者:默认全部动态,需要缓存就显式写 'use cache'。
这套模型配合 Partial Prerendering(PPR),能轻松实现“静态壳瞬间加载 + 动态内容流式渲染”,很多项目 TTFB 能下降 60%~80%。
直接copy老师的代码,main分支即可。然后编写.env文件。
xxxxxxxxxx11DATABASE_URL="postgresql://postgres:%21%40%23qaz12@192.168.31.198:5432/cache_components?sslmode=disable"安装依赖:npm install,npm install postgres。
配置drizzle:
xxxxxxxxxx111import { config } from "dotenv";2import { drizzle } from "drizzle-orm/postgres-js"; // 修改这里3import postgres from "postgres"; // 引入 postgres 客户端45config({ path: ".env" });67// 1. 创建数据库连接查询器8const queryClient = postgres(process.env.DATABASE_URL!);910// 2. 初始化 Drizzle11export const db = drizzle(queryClient);在第一次启动前,你需要同步数据库结构:
生成迁移文件(将代码中的 Schema 转换为 SQL):
xxxxxxxxxx11npm run db:generate
执行迁移(将结构应用到真实的数据库中):
xxxxxxxxxx11npm run db:migrate
(可选)填充数据:如果项目提供了一些初始测试数据,可以运行:
xxxxxxxxxx11npm run db:seed
npm run dev项目运行成功。
在nextjs 16之前,推荐在server components里面请求数据,因为更加接近服务端,请求会更快,而且nextjs提供的缓存功能。
xxxxxxxxxx161// src/app/page.tsx23export default async function Home() {4 const randomWord = await fetch("https://api.api-ninjas.com/v1/randomword", {5 headers: {6 "X-Api-Key": "a7W2X5UV2SO2p1rcJNNOPw==4PUAhX4EIcCzDbrx", // 需要去 https://api-ninjas.com 免费注册拿 key7 },8 }).then((r) => r.json());9 console.log("Home Component Ran");1011 return (12 <div className="flex flex-col min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">13 <h1 className="text-4xl font-bold">{randomWord.word[0]}</h1>14 </div>15 );16}在开发环境,是每次刷新都会请求数据:

但是放到生产环境,nextjs会将这个页面当作静态页面,里面的请求也只会执行一次。
npm run build

运行:npm run start,可以看到,只会在第一次发起请求,之后都不会了。

这个接口就是一个例子,虽然接口和接口参数都没有改变,但是实际上它的返回结果会不同,这时候nextjs的缓存反而是一种阻碍。
怎么样才能动态请求呢?参考:https://nextjs.org/docs/app/guides/caching#dynamic-rendering,有下面几种方法:

这就是nextjs v16之前,caching相关的概念。
为了开启nextjs v16中的cache components功能,需要在next.config.ts中配置cacheComponents: true。
xxxxxxxxxx71import type { NextConfig } from 'next'23const nextConfig: NextConfig = {4 cacheComponents: true, // 开启 Cache Components(包含 PPR)5}67export default nextConfig开启 cacheComponents(Next.js 15 末期 → 16+ 的实验/正式功能)之后,Next.js App Router 的默认行为发生了非常大的哲学转变,和之前版本(尤其是 Next.js 14 和早期 15)完全相反。
核心一句话总结:
开启 cacheComponents 后,默认一切数据获取都是动态的(运行时执行),除非你主动用 'use cache' 把某部分标记为要缓存。
| 方面 | 开启前(Next.js 14 ~ 15早期 / PPR 未完全稳定时) | 开启 cacheComponents 后(Next.js 16+ 推荐模式) | 变化方向 |
|---|---|---|---|
| fetch() 默认缓存 | 是(force-cache) | 否(相当于 no-store) | 从默认缓存 → 默认不缓存 |
| 非 fetch 数据源(数据库、第三方库) | 通常不缓存,除非用 React.cache 或 unstable_cache | 完全不缓存,必须显式用 'use cache' | 更严格 |
| 整个路由的渲染 | 倾向静态(build 时尽可能生成) | 默认动态(请求时渲染),静态壳 + 动态洞 | 更动态、更安全 |
| Partial Prerendering | 实验性,需要单独开启 | 默认行为(静态壳 + 流式动态内容) | 变成默认 |
| 要缓存某部分 | 比较难,需要各种 opts 或 force-static | 简单:加 'use cache' + 可选 cacheLife() | 显式、细粒度 |
| cookies/headers/searchParams | 让整条路由变动态 | 仍然让所在组件变动态,但可以包在 | 更可组合 |
| 构建时能生成多少静态内容 | 尽可能多 | 很少(只有纯计算、静态 JSX),数据部分默认被排除 | 构建更快、更安全 |
xxxxxxxxxx71// app/page.tsx2export default async function Page() {3 const data = await db.query() // ← 默认每次请求都重新查!不缓存4 const res = await fetch("https://api...") // ← 默认不缓存,等同 { cache: 'no-store' }56 return <div>{data.title}</div>7}↑ 这段代码在开启 cacheComponents 后,每次请求都会重新执行数据库查询和 fetch。
想让它缓存?必须显式写:
xxxxxxxxxx231// 现在推荐的现代写法2export default async function Page() {3 return (4 <>5 <StaticHeader /> {/* 自动进入静态壳 */}6 7 <Suspense fallback={<Loading />}>8 <CachedContent /> {/* 只有这里会缓存 */}9 </Suspense>10 </>11 )12}1314async function CachedContent() {15 'use cache' // ← 关键!标记要缓存16 cacheLife('hours') // 可选:默认大概 15 分钟 revalidate17 cacheTag('dashboard-data')1819 const data = await db.query()20 const res = await fetch("...")2122 return <div>{data.title}</div>23}以前:想动态要特意 opt-out 现在(cacheComponents 开启后):想缓存才特意 opt-in
Next.js App Router 中 Partial Prerendering (PPR) 的核心实现方式,目标是:让同一个路由里同时拥有极快的静态壳 + 可缓存的中频数据 + 实时动态内容,避免过去“要么全静要么全动”的两难选择。
原理:
在构建/预渲染时,Next.js 会尽量把页面渲染成一个静态 HTML 壳(static shell),里面包含所有能提前算完的内容。 真正动态/请求相关/网络请求的内容,则被推迟到请求时刻再渲染(通过 Suspense 流式传输)。
用 use cache 把能共享、变化不频繁的数据拉进静态壳, 用 Suspense 把每个用户、每次请求都不同的内容推到流式渲染, 就能在同一个页面里同时获得 极致的首屏速度 + 合理的实时性。
注意:use cache 和 Suspense 之间,没有强烈的关系。用了use cache,可能需要使用Suspense进行包裹,也可能不需要,关键是看有没有“运行时数据”,这一点要搞清楚。
| 情况 | 是否进入静态壳 | 处理方式 |
|---|---|---|
| 纯计算、模块导入、同步文件读取 | 是(自动) | 直接包含在静态 HTML 中 |
| fetch、数据库查询、异步操作 | 否(默认) | 必须显式处理,否则开发/构建报错 |
| 使用 cookies/headers/searchParams | 否(永远) | 只能放在 <Suspense>里 |
| 随机值 (Math.random()) 等 | 否(非确定性) | 必须推迟或用 "use cache" |
报错提示(很常见):
xxxxxxxxxx11Uncached data was accessed outside of <Suspense>| 手段 | 用途 | 代码示例 | 适用场景 |
|---|---|---|---|
| use cache | 主动把动态数据拉进静态壳 | 'use cache' cacheLife('hours') | 中频更新、共享数据(如文章、商品列表) |
<Suspense> | 把内容推迟到请求时渲染 | <Suspense fallback={<Loading/>}> | 个性化、实时数据、cookies 相关 |
| 不处理(默认) | 保持动态,每次请求都重新算 | — | 高度个性化、非常频繁变化的内容 |
开启了cacheComponents之后,默认情况就是动态请求。
什么情况下加Suspense包裹?一般来说,可以直接编写,如果nextjs要求你添加,它会报错。可以看一下这里:https://nextjs.org/docs/app/getting-started/cache-components#putting-it-all-together。但是呢,还是总结一下:
什么时候可以不用 Suspense?(推荐尽可能不包)
这种组件会直接被包含在静态 shell里,成为页面最快出现的部分。
运行时数据 = 只有在用户真正请求页面时(浏览器访问的那一刻),才能拿到或计算出来的数据。
它和“构建时/预渲染时就能确定的数据”形成鲜明对比。
运行时数据 vs 构建时数据(对比表)
| 特性 | 构建时数据(Build-time / Pre-rendered) | 运行时数据(Runtime) |
|---|---|---|
| 什么时候确定值 | 构建/部署时(next build 或 Vercel 部署时) | 用户请求页面时(每次访问都可能不同) |
| 是否能进入静态 HTML 壳 | 是(直接包含在生成的 HTML 里) | 否(必须推迟到请求时渲染,通常通过 Suspense) |
| 缓存情况(默认) | 可以被缓存('use cache' 或默认静态) | 默认不缓存(每次都重新计算) |
| 常见触发条件 | 纯计算、静态内容、'use cache' 的数据 | cookies、headers、searchParams、当前时间、用户数据 |
| 典型代表 | 文章正文、商品基本信息(不依赖登录)、分类列表 | 用户昵称、购物车数量、实时库存、个性化推荐 |
cookies() / headers() 相关
xxxxxxxxxx31import { cookies } from 'next/headers'23const theme = cookies().get('theme')?.value // 运行时数据searchParams(URL 查询参数)
xxxxxxxxxx31export default function Page({ searchParams }) {2 const sort = searchParams.sort || 'newest' // 运行时3}当前登录用户数据
xxxxxxxxxx11const user = await getCurrentUser() // 依赖 session/cookies实时/时间敏感数据
xxxxxxxxxx11const now = new Date().toLocaleString() // 每次请求不同数据库查询 + 用户相关过滤
xxxxxxxxxx31const orders = await db.orders.findMany({2 where: { userId: currentUser.id } // 依赖当前用户 → 运行时3})外部 API 调用 + 带 token/认证
xxxxxxxxxx31const data = await fetch('/api/user-data', {2 headers: { Authorization: `Bearer ${token}` }3})xxxxxxxxxx71// 这部分是运行时数据,必须用 Suspense 包裹2<Suspense fallback={<div>加载中...</div>}>3 <UserProfile /> // 里面用了 cookies() 和 await getUser()4</Suspense>56// 这部分不是运行时数据,可以不包 Suspense(或包了也无所谓)7<ProductList /> // 如果里面用了 'use cache',就能进静态壳一句话总结
运行时数据 = “用户一进来就知道不一样”的数据 它包括:
xxxxxxxxxx91'use cache' // 标记这个组件/函数要被缓存23cacheLife('hours') // 内置时长:'seconds'|'minutes'|'hours'|'days'|'forever'4cacheLife({ stale: 3600 }) // 细粒度控制(stale-while-revalidate 风格)56cacheTag('products') // 打标签7// 之后可以在 Server Action 里:8updateTag('products') // 立即失效9revalidateTag('products', 'max') // 延迟失效xxxxxxxxxx291// app/page.tsx2export default async function Page() {3 return (4 <div>5 {/* 以下部分会进入静态壳 */}6 <h1>欢迎</h1>7 <StaticHeader /> {/* 纯静态 */}8 9 {/* 缓存几小时的共享数据 */}10 <Suspense fallback={<p>加载中...</p>}>11 <BlogPosts /> {/* 里面用了 'use cache' + cacheLife('hours') */}12 </Suspense>1314 {/* 每次请求都不同的实时内容 */}15 <Suspense fallback={<Skeleton />}>16 <UserProfile /> {/* 用 cookies、当前用户数据 */}17 </Suspense>18 </div>19 )20}2122async function BlogPosts() {23 'use cache'24 cacheLife('hours')25 cacheTag('blog-posts')2627 const posts = await db.select().from(postsTable)28 return <PostList posts={posts} />29}"use cache")如果一个组件,比如说header、footer、nav等,里面的代码都是静态代码,还需要特地指定 'use cache' 吗?
在 Next.js 16 中(开启 cacheComponents: true 的情况下),对于完全静态的组件(header、footer、nav 等),里面没有任何动态操作(无 fetch、无 cookies、headers、searchParams、无数据库查询等),你通常已经不需要特地加 'use cache' 了。
如果不加不放心,可以查看waterfall、TTFB,这些到时候一查就会了。
比较健康的 TTFB 参考值(生产环境,75 分位):
| TTFB 值 | 评价 | 建议 |
|---|---|---|
| < 200ms | 非常优秀 | 静态/边缘缓存/好的服务器 |
| 200~500ms | 正常/可接受 | 大部分 Next.js 项目在这个区间 |
| 500~800ms | 需要关注 | 可能有动态渲染或冷启动问题 |
| > 800ms | 明显拖慢体验 | 必须优化(PPR、cache、ISR 等) |
下面是目前(2026年1月)Next.js 16 的实际情况对比表,方便理解:
xxxxxxxxxx71场景 | 是否默认静态/可缓存 | 需不需要加 "use cache" | 推荐做法2------------------------------------------|---------------------------|-----------------------------|----------------------3完全纯静态组件(纯 JSX + CSS + 硬编码) | 是(会被包含在静态壳里) | **不需要** | 不加(最干净)4静态组件,但放在动态页面/布局里使用 | 通常也会被静态化 | **不需要**(但可加做保险) | 建议先不加,看效果5组件里有 fetch 但希望缓存(常见场景) | **不会**自动缓存 | **必须加** | 加 "use cache"6组件里有 cookies()/headers() 等 | **不会**自动缓存 | **必须加**(且通常要 private)| 加 "use cache: private"7想非常明确地告诉团队「这个组件要缓存」 | — | **可以加**(文档化用途) | 加(当做最佳实践)目前社区最常见的实际做法(2025年底~2026年初)
1// app/(marketing)/layout.tsx2import Header from "@/components/marketing/Header";3import Footer from "@/components/marketing/Footer";45export default function MarketingLayout({ children }: { children: React.ReactNode }) {6 return (7 <>8 {/* 绝大多数团队现在都不在这里加 use cache */}9 <Header /> 10 {children}11 <Footer />12 </>13 );14}1// components/marketing/Header.tsx2// 目前最主流的写法:**不加 use cache**34export default function Header() {5 return (6 <header className="...">7 <nav>...</nav>8 <div className="logo">XXX</div>9 {/* 纯静态内容 */}10 </header>11 );12}什么时候你应该加 'use cache' 到 header/footer 上?
'use cache' 强制纳入静态壳cacheComponents: true,但发现某些纯静态组件在动态路由里没有被很好地复用

在构建/预渲染阶段,Next.js 会尽可能生成一个静态 HTML 壳(static shell),里面包含所有能提前算完的 UI 和内容。 真正需要请求时才能确定或每次都不同的动态部分,则被推迟到用户实际访问时再渲染,并通过流式传输(streaming)逐步填充到静态壳中。结果:用户几乎瞬间看到页面框架(通常 <100ms),动态内容随后流式出现,既快又新鲜。
预渲染阶段(build time 或首次请求时) Next.js 从上到下遍历组件树:
能同步完成的部分(纯 JSX、import、fs.readFileSync、纯计算等)→ 直接渲染进静态 HTML 壳
遇到无法完成的部分(fetch、数据库查询、cookies()、headers()、searchParams、Date.now()、Math.random() 等)→ 必须显式处理:
响应阶段(用户访问时)
客户端导航(点击链接跳转)
在 Next.js 16 中(开启 Cache Components 后),已经不再像以前那样明确区分「static components」和「dynamic components」这两个类别了。
取而代之的是一个更细粒度、更自动化的混合模型:Partial Prerendering (PPR) + Cache Components,核心思想变成了:
“页面默认是动态的,但 Next.js 会尽量自动提取出能预渲染的部分,生成一个静态 HTML shell,剩下的部分通过 Suspense 变成动态洞(holes),或者用 'use cache' 主动拉回静态 shell。”
Static Shell 部分(自动的静态内容)
Cached Content 部分(主动缓存的“曾经动态”内容)
Dynamic Holes / Runtime 部分(真正的运行时内容)
<Suspense> 包裹使用use cache可以缓存函数、组件或者文件。

xxxxxxxxxx661import { db } from "@/db/drizzle";2import { products } from "@/db/schema";3import { connection } from "next/server";4import { Suspense } from "react";56export default async function Home() {7 console.log("Home Component Ran");89 return (10 <div className="flex flex-col min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">11 <h1 className="text-4xl font-bold">Hello</h1>12 <div className="p-2 mt-2 bg-violet-800">13 <Suspense fallback={<div className="text-2xl">Loading...</div>}>14 <DynamicWord />15 </Suspense>16 </div>17 <div className="p-2 mt-2 bg-green-800">18 <StaticWord />19 </div>20 <div className="p-2 mt-2 bg-blue-800">21 <Suspense fallback={<div className="text-2xl">Loading...</div>}>22 <FeaturedProduct />23 </Suspense>24 </div>25 <div className="p-2 mt-2 bg-red-800">26 <Suspense fallback={<div className="text-2xl">Loading...</div>}>27 <RandomNumber />28 </Suspense>29 </div>30 </div>31 );32}3334async function RandomNumber() {35 console.log("RandomNumber Component Ran");36 await connection();37 return <div className="text-2xl">Random Number: {Math.random() * 10}</div>;38}3940async function FeaturedProduct() {41 console.log("FeaturedProduct Component Ran");42 const [product] = await db.select().from(products).limit(1);43 return (44 <div>45 <h2 className="text-2xl">{product.title}</h2>46 <p>{product.price}</p>47 </div>48 );49}5051async function DynamicWord() {52 console.log("DynamicWord Ran");53 const { word } = await fetch(`http://localhost:4000/words/random-word`).then(54 (res) => res.json()55 );56 return <div className="text-2xl">Dynamic Random Word: {word}</div>;57}5859async function StaticWord() {60 "use cache";61 console.log("StaticWord Ran");62 const { word } = await fetch(`http://localhost:4000/words/random-word`).then(63 (res) => res.json()64 );65 return <div className="text-2xl">Static Random Word: {word}</div>;66}重点就是,Dynamic Holes / Runtime 部分必须使用Suspense进行包裹;使用use cache来缓存内容。
可以看到,只有Static random word是静态内容,其余都是动态的。

接下来的三节课,都使用products/[id]/page.tsx这个页面来说明各种cache规则到底应该在哪里做、怎么做。
先构建页面,并将数据请求的函数放到utils.ts文件里面。
xxxxxxxxxx341// src/app/products/[id]/page.tsx23import { getProduct, getRecommendedProducts } from "./utils";45export default async function Page({6 params,7}: {8 params: Promise<{ id: string }>;9}) {10 const { id } = await params;11 const product = await getProduct(+id);12 const recommendedProducts = await getRecommendedProducts();13 return (14 <div className="p-4">15 <h1 className="text-3xl bg-violet-800 mb-2 font-bold p-2">16 {product.title}17 </h1>18 <p className="text-2xl bg-pink-800 p-2 mb-2 font-medium">19 {product.price}20 </p>21 <p className="font-lg bg-green-700 p-2 mb-2">{product.description}</p>22 <div className="bg-blue-900 p-2 mt-4">23 <h2 className="font-bold text-2xl mb-2">Recommended Products</h2>24 <ul>25 {recommendedProducts.map((recommendedProduct) => (26 <li key={recommendedProduct.id} className="p-2 mb-2 bg-blue-950">27 {recommendedProduct.title}28 </li>29 ))}30 </ul>31 </div>32 </div>33 );34}xxxxxxxxxx311src/app/products/[id]/utils.ts23import { db } from "@/db/drizzle";4import { products } from "@/db/schema";5import { eq } from "drizzle-orm";6import { cookies } from "next/headers";78export async function getProduct(id: number) {9 const product = await db10 .select()11 .from(products)12 .where(eq(products.id, id))13 .limit(1);14 return product[0];15}1617export async function getProductPrice(id: number) {18 const product = await db19 .select({ price: products.price })20 .from(products)21 .where(eq(products.id, id))22 .limit(1);23 return product[0]?.price;24}2526export async function getRecommendedProducts() {27 const sessionId = (await cookies()).get("token")?.value;28 console.log("Session ID:", sessionId);29 const recommendedProducts = await db.select().from(products).limit(3);30 return recommendedProducts;31}产品的title和description基本不变,所以可以做缓存。而price的变化也不是太频繁,但是会变,所以单独使用一种cache策略use cahce: remote,下节课会将。recommendedProducts这个就经常变化了,开启了cacheComponents之后,可以不处理。
这节课先解决title和description的缓存问题。
因为title和description都是从getProduct这个请求接口返回的,所以直接在这个函数里面使用use cache即可。

可以看到,页面有一个报错:


报错原因:
在 页面顶层(Page 组件) 直接 await params 和 await getProduct(+id),而 Next.js 16(开启 Cache Components / Partial Prerendering 后)把这个行为视为访问了运行时不确定数据。
在动态路由 [id] 中:
结果:Next.js 故意抛这个错误来强制你做出选择:
<Suspense>(允许流式渲染)老师是使用generateStaticParams来解决。设置一些静态页面。
generateStaticParams 是 Next.js 的官方 API,专门用于 App Router(app 目录)下的动态路由(dynamic routes),它的作用是在构建时(build time)提前告诉 Next.js 要为哪些动态参数生成静态页面。
xxxxxxxxxx401import { db } from "@/db/drizzle";2import { getProduct, getRecommendedProducts } from "./utils";3import { products } from "@/db/schema";45export default async function Page({6 params,7}: {8 params: Promise<{ id: string }>;9}) {10 const { id } = await params;11 const product = await getProduct(+id);12 const recommendedProducts = await getRecommendedProducts();13 return (14 <div className="p-4">15 <h1 className="text-3xl bg-violet-800 mb-2 font-bold p-2">16 {product.title}17 </h1>18 <p className="text-2xl bg-pink-800 p-2 mb-2 font-medium">19 {product.price}20 </p>21 <p className="font-lg bg-green-700 p-2 mb-2">{product.description}</p>22 <div className="bg-blue-900 p-2 mt-4">23 <h2 className="font-bold text-2xl mb-2">Recommended Products</h2>24 <ul>25 {recommendedProducts.map((recommendedProduct) => (26 <li key={recommendedProduct.id} className="p-2 mb-2 bg-blue-950">27 {recommendedProduct.title}28 </li>29 ))}30 </ul>31 </div>32 </div>33 );34}3536// 在构建时(build time)提前告诉 Next.js 要为哪些动态参数生成静态页面。这里是选了5个。37export async function generateStaticParams() {38 const ids = await db.select({ id: products.id }).from(products).limit(5);39 return ids.map(({ id }) => ({ id: id.toString() }));40}可以看到,正常了,渲染速度是非常快的。

把 await params 和初始数据获取下沉到子组件里,也就是将 params 作为 Promise 向下传递 → 在子组件里 await,并用 Suspense 包裹:
xxxxxxxxxx351// products/[id]/page.tsx2import { Suspense } from "react";3import { getProduct } from "./utils";45export default function Page({ params }: { params: Promise<{ id: string }> }) {6 return (7 <div className="p-4">8 <Suspense9 fallback={<div className="text-2xl animate-pulse">加载商品中...</div>}>10 <ProductContent params={params} />11 </Suspense>12 </div>13 );14}1516// 新建 ProductContent.tsx(或直接写在下面)17async function ProductContent({ params }: { params: Promise<{ id: string }> }) {18 const { id } = await params;19 const product = await getProduct(+id);20 // const recommendedProducts = await getRecommendedProducts()2122 return (23 <>24 <h1 className="text-3xl bg-violet-800 mb-2 font-bold p-2">25 {product.title}26 </h1>27 <p className="text-2xl bg-pink-800 p-2 mb-2 font-medium">28 {product.price}29 </p>30 <p className="font-lg bg-green-700 p-2 mb-2">{product.description}</p>31 {/* 推荐商品... */}32 </>33 );34}35效果我反正看不出区别。

虽然 use cache 指令对于大多数应用需求已经足够,但你偶尔可能会注意到,缓存的操作比预期更频繁地重新执行,或者你的上游服务(CMS、数据库、外部 API)收到的请求比你预期的要多。这是因为内存内缓存(in-memory caching)存在固有的局限性:
需要注意的是,use cache 提供的价值远超单纯的服务器端缓存:它会告知 Next.js 哪些内容可以被预获取(prefetch),并为客户端导航定义过时时间(stale times)。
'use cache: remote' 指令允许你以声明式的方式指定,将缓存的输出存储在远程缓存中,而不是内存中。虽然这为特定操作提供了更持久的缓存,但也伴随着权衡取舍:基础设施成本以及缓存查找时的网络延迟。
price采用这种缓存方式。获取price单独创建一个接口,然后使用use cache: remote来做缓存。

然后为price单独做一个组件,使用Suspense来包裹:

connection的意思:“我故意告诉 Next.js:别把我放进静态壳里,我要在每次请求时都重新跑一遍!”它是一个调试/教学/强制动态的小技巧,而不是正常业务代码中应该长期保留的东西。
可以看到,price只在第一次请求了数据,然后就缓存了数据。


这是一个实验性的命令。
'use cache: private' 指令允许函数在缓存作用域内直接访问运行时请求 API,例如 cookies()、headers()和 searchParams。
然而,结果永远不会存储在服务器上,它们仅在浏览器内存中缓存,并且在页面重新加载后不会持久存在。
何时使用 'use cache: private'
由于这个指令会访问运行时数据,因此该函数会在每次服务器渲染时都执行,并且在静态壳(static shell)生成阶段被完全排除,不会参与预渲染。无法为 'use cache: private' 配置自定义缓存处理器(custom cache handlers)。
推荐product列表使用了session来获取,是针对不同用户有不同的推荐列表。那么同一个用户,在不同product详情里面,展示的推荐列表应该是一致的,所以这些数据应该被缓存。但是它又不是一直不变,我们需要的是缓存时间稍微折中一点,所以用上了这个命令:use cache: private。
接口获取函数里面使用use cache: private:

然后将相应的逻辑提取出来,单独做一个组件:

可以看到,不同的详情页面,推荐列表只请求了一次,其余的情况都使用缓存。

核心对比表
| 特性 | cacheLife | revalidatePath |
|---|---|---|
| 作用对象 | 单个函数/组件(Cache Component) | 整条路径(route/path) |
| 控制粒度 | 非常细(具体到某个数据查询) | 比较粗(整个页面或路由段) |
| 主要目的 | 设置这个缓存多长时间后应该重新生成/验证 | 手动使某条路径的缓存失效(立即或延迟) |
| 使用位置 | 函数/组件内部,搭配 'use cache' | Server Action、Route Handler、Server Component |
| 生效时机 | 构建时/首次请求时 + 后续 revalidate 时 | 人为触发时(按钮点击、表单提交等) |
| 是否立即生效 | 否(等到时间到期或手动触发) | 可以立即(revalidatePath(path, 'page')) |
| 典型场景 | 商品详情、文章内容、分类列表(中频更新) | 用户提交评论后刷新文章页、修改配置后刷新设置页 |
| 是否会影响静态壳 | 是(会决定是否能长期进入预渲染) | 是(会使整条路径重新生成静态壳) |
| 当前状态 | 稳定推荐 | 稳定,但逐渐被 revalidateTag 部分取代 |
xxxxxxxxxx71'use cache'23cacheLife('hours') // 内置快捷方式4// 等价于:5cacheLife({ expire: 3600 }) // 1 小时后过期67cacheLife({ stale: 300, expire: 3600 }) // 更细粒度:5分钟 stale-while-revalidate常见内置快捷方式(推荐优先使用):
xxxxxxxxxx51cacheLife('seconds') // 极短,通常用于测试2cacheLife('minutes') // 1 分钟3cacheLife('hours') // 1 小时(最常用)4cacheLife('days') // 1 天5cacheLife('forever') // 几乎永不过期(除非手动失效)典型用法:
xxxxxxxxxx71async function getProduct(id: number) {2 'use cache'3 cacheLife('hours') // 这个商品信息缓存 1 小时4 cacheTag(`product-${id}`) // 方便后面手动失效56 return db.select().from(products).where(eq(products.id, id))7}效果: 这个函数的结果会在1小时内被复用,即使在不同用户、不同 serverless 实例之间(如果用了 use cache: remote 则更可靠)。 超过 1 小时后,下次请求会重新执行(background revalidation)。
用途:用户刚修改了数据,我希望页面立刻更新;我想在操作完成后刷新整个页面;我想让多个页面同时失效(比如所有文章)。
xxxxxxxxxx101// 在 Server Action 中最常用2'use server'34export async function updateProduct(formData: FormData) {5 // ... 更新数据库 ...67 revalidatePath('/products/[id]', 'page') // 立即使具体商品页失效8 // 或9 revalidatePath('/products', 'layout') // 使整个 products 目录下的所有页面失效10}第二个参数选项(很重要!):
| 选项 | 含义 | 影响范围 | 推荐场景 |
|---|---|---|---|
| 'page' | 只失效当前 path 的页面 | 最小(最精准) | 编辑单个商品后刷新详情页 |
| 'layout' | 失效包含这个 path 的所有 layout + page | 中等 | 修改全局导航/侧边栏后刷新 |
| 无(默认) | 相当于 'page' | 同 'page' | 大多数情况 |
cacheTag、revalidateTag 和 updateTag 是 Next.js 16+(Cache Components 体系)中非常强大且推荐的缓存失效机制,它们构成了目前最灵活、最细粒度的标签式缓存管理方式。
下面用最清晰的对比和实际场景来讲解这三个 API 的作用、区别和推荐用法
| API | 作用时机 | 作用对象 | 粒度 | 典型使用位置 | 是否立即生效 | 是否实验性 |
|---|---|---|---|---|---|---|
cacheTag | 定义时 | 给某个缓存打标签 | 非常细 | 'use cache' 函数内部 | — | 稳定 |
revalidateTag | 运行时触发 | 使打过这个 tag 的所有缓存失效 | 细到中等 | Server Action / Route Handler | 是(可配置延迟) | 稳定 |
updateTag | 运行时触发 | 立即使打过这个 tag 的缓存失效 | 细到中等 | Server Action | 立即 | 实验性 |
作用:给当前这个 'use cache' 的函数/组件打上一个或多个标签(tag),方便后续批量失效。
语法(通常和 cacheLife 一起用):
xxxxxxxxxx91async function getProduct(id: number) {2 'use cache'3 cacheLife('hours')4 cacheTag('products') // 通用标签:所有商品5 cacheTag(`product-${id}`) // 具体标签:这个商品6 cacheTag('expensive-queries') // 业务分组标签78 return db.select().from(products).where(eq(products.id, id))9}效果: 这个函数的缓存结果会被关联到 products、product-123、expensive-queries 这三个标签。 后续只要失效其中任何一个标签,这个缓存就会被重新生成。
作用: 手动触发:让所有打了这个 tag 的缓存失效(下次请求时重新执行)。
常用位置:Server Action、Route Handler
语法:
xxxxxxxxxx151'use server'23export async function updateProduct(id: number, data: any) {4 await db.updateProduct(id, data)56 // 推荐方式:失效具体商品 + 通用商品列表7 revalidateTag(`product-${id}`)8 revalidateTag('products')910 // 或者更激进:一次性失效多个标签11 revalidateTag('expensive-queries')1213 // 可选:延迟失效(避免瞬间峰值)14 revalidateTag('products', { delay: 5000 }) // 5秒后失效15}与 revalidatePath 的最大区别:
revalidatePath:失效整条路径(可能重新渲染很多无关内容)revalidateTag:只失效打了这个 tag 的具体数据函数(更精准、更省资源)推荐优先级(2025 年底社区共识):
xxxxxxxxxx11revalidateTag > revalidatePath > revalidate(旧 ISR)作用:
立即使打了这个 tag 的所有缓存失效,并且在当前请求中就使用最新的数据(不像 revalidateTag 是“下次请求再更新”)。
语法(目前仍实验性):
xxxxxxxxxx121'use server'23export async function publishArticle(id: number) {4 await db.publishArticle(id)56 // 立即失效 + 当前请求就能看到最新版本7 updateTag(`article-${id}`)8 updateTag('published-articles')910 // 可选:同时更新多个11 updateTag('homepage-featured')12}关键区别:
revalidateTag:延迟(下一次访问时才重新生成)updateTag:立即(当前这次 Server Action 就能拿到最新值)适用场景:
xxxxxxxxxx241// 数据层(定义标签)2async function getProduct(id: number) {3 'use cache'4 cacheLife('hours')5 cacheTag('products')6 cacheTag(`product-${id}`)78 return db.products.find(id)9}1011// Server Action(精细失效)12'use server'13export async function updateProductPrice(id: number, newPrice: number) {14 await db.updatePrice(id, newPrice)1516 // 最精准:只失效这个商品17 revalidateTag(`product-${id}`)1819 // 如果价格变动影响列表,也失效通用标签20 revalidateTag('products')2122 // 如果是重要更新,想立即看到效果23 // updateTag(`product-${id}`) // 实验性,谨慎使用24}总结:三者的定位与选择指南
xxxxxxxxxx71什么时候用哪个?2├── 我想给缓存打上“可被批量管理的身份” → cacheTag3├── 用户操作后,下次访问希望看到更新 → revalidateTag(主流首选)4└── 用户操作后,**当前页面立刻**就要看到最新结果 → updateTag(实验,慎用)56粒度排序(越精准越好):7revalidateTag(具体 tag) > revalidatePath(具体 path) > revalidatePath(..., 'layout')